/* HTTP Restful API Server

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <fcntl.h>
#include "esp_http_server.h"
#include "esp_chip_info.h"
#include "esp_random.h"
#include "esp_log.h"
#include "esp_vfs.h"
#include "cJSON.h"

#include "app_uart.h"
#include "tcp_server.h"

static const char *TAG = "webserver";

static const char *REST_TAG = "esp-rest";
#define REST_CHECK(a, str, goto_tag, ...)                                              \
    do                                                                                 \
    {                                                                                  \
        if (!(a))                                                                      \
        {                                                                              \
            ESP_LOGE(REST_TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \
            goto goto_tag;                                                             \
        }                                                                              \
    } while (0)

#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128)
#define SCRATCH_BUFSIZE (10240)

typedef struct rest_server_context
{
    char base_path[ESP_VFS_PATH_MAX + 1];
    char scratch[SCRATCH_BUFSIZE];
} rest_server_context_t;

#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0)

// HTTP服务器句柄
static httpd_handle_t server = NULL;
// 仅允许单一WebSocket客户端连接，保存该连接对应的fd
static int ws_client_fd = -1;

esp_err_t webserver_ws_send_data(const char *data, size_t len)
{
    if (server == NULL || ws_client_fd == -1)
    {
        return ESP_FAIL;
    }

    // printf("%d, %s\n", len, data);

    httpd_ws_frame_t ws_pkt = {
        .payload = (uint8_t *)data,
        .len = len,
        .type = HTTPD_WS_TYPE_TEXT,
    };
    // 采用异步发送接口
    esp_err_t ret = httpd_ws_send_frame_async(server, ws_client_fd, &ws_pkt);
    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "Failed to send data, fd=%d", ws_client_fd);
    }
    return ret;
}

/* Set HTTP response content type according to file extension */
static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filepath)
{
    const char *type = "text/plain";
    if (CHECK_FILE_EXTENSION(filepath, ".html"))
    {
        type = "text/html";
    }
    else if (CHECK_FILE_EXTENSION(filepath, ".js"))
    {
        type = "application/javascript";
    }
    else if (CHECK_FILE_EXTENSION(filepath, ".css"))
    {
        type = "text/css";
    }
    else if (CHECK_FILE_EXTENSION(filepath, ".png"))
    {
        type = "image/png";
    }
    else if (CHECK_FILE_EXTENSION(filepath, ".ico"))
    {
        type = "image/x-icon";
    }
    else if (CHECK_FILE_EXTENSION(filepath, ".svg"))
    {
        type = "text/xml";
    }
    return httpd_resp_set_type(req, type);
}

/* Send HTTP response with the contents of the requested file */
static esp_err_t rest_common_get_handler(httpd_req_t *req)
{
    char filepath[FILE_PATH_MAX];

    rest_server_context_t *rest_context = (rest_server_context_t *)req->user_ctx;
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    if (req->uri[strlen(req->uri) - 1] == '/')
    {
        strlcat(filepath, "/index.html", sizeof(filepath));
    }
    else
    {
        strlcat(filepath, req->uri, sizeof(filepath));
    }
    int fd = open(filepath, O_RDONLY, 0);
    if (fd == -1)
    {
        ESP_LOGE(REST_TAG, "Failed to open file : %s", filepath);
        /* Respond with 500 Internal Server Error */
        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
        return ESP_FAIL;
    }

    set_content_type_from_file(req, filepath);

    char *chunk = rest_context->scratch;
    ssize_t read_bytes;
    do
    {
        /* Read file in chunks into the scratch buffer */
        read_bytes = read(fd, chunk, SCRATCH_BUFSIZE);
        if (read_bytes == -1)
        {
            ESP_LOGE(REST_TAG, "Failed to read file : %s", filepath);
        }
        else if (read_bytes > 0)
        {
            /* Send the buffer contents as HTTP response chunk */
            if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK)
            {
                close(fd);
                ESP_LOGE(REST_TAG, "File sending failed!");
                /* Abort sending file */
                httpd_resp_sendstr_chunk(req, NULL);
                /* Respond with 500 Internal Server Error */
                httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
                return ESP_FAIL;
            }
        }
    } while (read_bytes > 0);
    /* Close file after sending complete */
    close(fd);
    ESP_LOGI(REST_TAG, "File sending complete");
    /* Respond with an empty chunk to signal HTTP response completion */
    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

/* Simple handler for getting system handler */
static esp_err_t system_info_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "application/json");
    cJSON *root = cJSON_CreateObject();
    esp_chip_info_t chip_info;
    esp_chip_info(&chip_info);
    cJSON_AddStringToObject(root, "version", IDF_VER);
    cJSON_AddNumberToObject(root, "cores", chip_info.cores);
    const char *sys_info = cJSON_Print(root);
    httpd_resp_sendstr(req, sys_info);
    free((void *)sys_info);
    cJSON_Delete(root);
    return ESP_OK;
}

static esp_err_t echo_handler(httpd_req_t *req)
{
    if (req->method == HTTP_GET)
    {
        ESP_LOGI(TAG, "Handshake done, the new connection was opened");
        return ESP_OK;
    }

    ws_client_fd = httpd_req_to_sockfd(req);

    httpd_ws_frame_t ws_pkt;
    uint8_t *buf = NULL;
    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;

    /* Set max_len = 0 to get the frame len */
    esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret);
        return ret;
    }

    ESP_LOGI(TAG, "frame len is %d", ws_pkt.len);
    if (ws_pkt.len)
    {
        /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */
        buf = calloc(1, ws_pkt.len + 1);
        if (buf == NULL)
        {
            ESP_LOGE(TAG, "Failed to calloc memory for buf");
            return ESP_ERR_NO_MEM;
        }
        ws_pkt.payload = buf;
        /* Set max_len = ws_pkt.len to get the frame payload */
        ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
        if (ret != ESP_OK)
        {
            ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret);
            free(buf);
            return ret;
        }
        ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload);

        // parser JSON
        {
            // 解析 JSON 字符串
            cJSON *root = cJSON_Parse((char *)ws_pkt.payload);
            if (root == NULL)
            {
                ESP_LOGE(TAG, "cJSON_Parse failed.");
                return ESP_FAIL;
            }

            // 获取 "type" 字段
            cJSON *type_item = cJSON_GetObjectItem(root, "type");
            if (cJSON_IsString(type_item) && (type_item->valuestring != NULL))
            {
                ESP_LOGI(TAG, "type: %s", type_item->valuestring);
                if (strcmp("control", type_item->valuestring) == 0)
                {
                    cJSON *command_item = cJSON_GetObjectItem(root, "command");
                    if (cJSON_IsString(command_item) && (command_item->valuestring != NULL))
                    {
                        ESP_LOGI(TAG, "command: %s", command_item->valuestring);
                        app_uart_send((uint8_t *)command_item->valuestring, strlen(command_item->valuestring));
                        // if (strcmp("cmd1", type_item->valuestring) == 0)
                        // {
                        //     app_uart_send("cmd1\n");
                        // }
                        // else if (strcmp("cmd2", type_item->valuestring) == 0)
                        // {
                        // }
                        // else if (strcmp("cmd3", type_item->valuestring) == 0)
                        // {
                        // }
                        // else if (strcmp("cmd4", type_item->valuestring) == 0)
                        // {
                        // }
                        // else if (strcmp("cmd5", type_item->valuestring) == 0)
                        // {
                        // }
                    }
                }
                else if (strcmp("tcp_server_control", type_item->valuestring) == 0)
                {
                    cJSON *command_item = cJSON_GetObjectItem(root, "command");
                    if (cJSON_IsString(command_item) && (command_item->valuestring != NULL))
                    {
                        ESP_LOGI(TAG, "command: %s", command_item->valuestring);
                        tcp_server_send(tcp_server_get_fd(),
                                        (uint8_t *)command_item->valuestring,
                                        strlen(command_item->valuestring));
                    }
                }
            }
            else
            {
                ESP_LOGE(TAG, "Invalid type");
            }

            cJSON_Delete(root);
        }
    }
    ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type);

    // echo back
    ret = httpd_ws_send_frame(req, &ws_pkt);
    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
    }
    free(buf);

    return ret;
}

// static esp_err_t ws_handler(httpd_req_t *req)
// {
//     int fd = httpd_req_to_sockfd(req);

//     // 如果已有连接，则拒绝新连接
//     if (ws_client_fd != -1)
//     {
//         ESP_LOGE(TAG, "Websocket already connected, fd=%d", fd);
//         return ESP_FAIL;
//     }

//     ws_client_fd = fd;
//     ESP_LOGI(TAG, "WebSocket client connected, fd=%d", fd);

//     uint8_t buf[128];
//     int flags = 0;
//     // 使用while循环持续读取数据，确保长连接不断接收信息
//     while (1)
//     {
//         int ret = httpd_ws_recv_frame(req, buf, sizeof(buf));
//         if (ret <= 0)
//         {
//             break;
//         }
//         ESP_LOGI(TAG, "收到WebSocket消息: %.*s", ret, buf);

//         // 调用用户注册的回调函数处理接收到的数据
//         // if (ws_recv_cb)
//         // {
//         //     ws_recv_cb((const char *)buf, ret);
//         // }
//         // 如果需要回显数据，可在回调中自行调用 webserver_ws_send_data() 实现
//     }
//     ESP_LOGI(TAG, "WebSocket客户端断开, fd=%d", fd);
//     ws_client_fd = -1;
//     return ESP_OK;
// }

static const httpd_uri_t ws = {
    .uri = "/ws",
    .method = HTTP_GET,
    .handler = echo_handler,
    .user_ctx = NULL,
    .is_websocket = true,
};

esp_err_t start_rest_server(const char *base_path)
{
    REST_CHECK(base_path, "wrong base path", err);
    rest_server_context_t *rest_context = calloc(1, sizeof(rest_server_context_t));
    REST_CHECK(rest_context, "No memory for rest context", err);
    strlcpy(rest_context->base_path, base_path, sizeof(rest_context->base_path));

    // httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.uri_match_fn = httpd_uri_match_wildcard;

    ESP_LOGI(REST_TAG, "Starting HTTP Server");
    REST_CHECK(httpd_start(&server, &config) == ESP_OK, "Start server failed", err_start);

    httpd_uri_t system_info_get_uri = {
        .uri = "/api/v1/system/info",
        .method = HTTP_GET,
        .handler = system_info_get_handler,
        .user_ctx = rest_context,
    };
    httpd_register_uri_handler(server, &system_info_get_uri);

    httpd_register_uri_handler(server, &ws);

    /* URI handler for getting web server files */
    httpd_uri_t common_get_uri = {
        .uri = "/*",
        .method = HTTP_GET,
        .handler = rest_common_get_handler,
        .user_ctx = rest_context};
    httpd_register_uri_handler(server, &common_get_uri);

    return ESP_OK;
err_start:
    free(rest_context);
err:
    return ESP_FAIL;
}
